Skip to content

[pkg] Resolve directory symlinks in fetched targets#13792

Open
ElectreAAS wants to merge 10 commits intoocaml:mainfrom
ElectreAAS:push-xmyqtonwkonu
Open

[pkg] Resolve directory symlinks in fetched targets#13792
ElectreAAS wants to merge 10 commits intoocaml:mainfrom
ElectreAAS:push-xmyqtonwkonu

Conversation

@ElectreAAS
Copy link
Copy Markdown
Collaborator

@ElectreAAS ElectreAAS commented Mar 13, 2026

Housekeeping

This PR fixes the tests in #13393
#9873 will not be fixed, but still a significant step towards fixing #13678.

What this PR does

After fetching package sources, add a pass resolving directory symlinks. As they're not problematic at this stage, file symlinks are left as is. Broken symlinks are removed silently to preserve existing behaviour.

Note: both portable_hardlink and portable_symlink work backwards from what I initially understood, see #13791

Done with the help of @Alizter, so thanks :)

Comment thread test/blackbox-tests/test-cases/pkg/source-with-directory-symlink.t
@ElectreAAS ElectreAAS requested review from art-w and rgrinberg March 16, 2026 17:25
@shonfeder shonfeder requested a review from Alizter March 19, 2026 13:08
@Alizter Alizter mentioned this pull request Mar 20, 2026
15 tasks
Copy link
Copy Markdown
Collaborator

@Alizter Alizter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is an example that I think causes a loop in the current algorithm:

  /source/
    dir_a/
      link_to_b -> ../dir_b
    dir_b/
      link_to_a -> ../dir_a

Following this along we get an infinitely descending directory.

I think you will need to keep a set of resolved targets and check against it if you want to remember where you have been as to not repeat the resolution.

Another issue that I see is the multiple passes that are currently done, but we can improve this later once we have something that works correctly.

We also might need to add some validation that we don't escape the source directory, I don't think its a good idea to allow symlinks to / for example.

@ElectreAAS ElectreAAS marked this pull request as draft March 26, 2026 22:43
@ElectreAAS
Copy link
Copy Markdown
Collaborator Author

I just pushed work in progress to adress the latest comment about cycles.
A basic cycle detection mechanism is in place, and works in simple scenarios (basic-cycle.t), but fails in more complex ones (link-to-parent.t) and crashes the sandboxing mechanisms of cram tests. Fun stuff

@Alizter Alizter self-requested a review March 27, 2026 07:56
Comment thread src/dune_pkg/fetch.ml Outdated
Comment thread src/dune_pkg/fetch.ml Outdated
This fails correctly
$ build_pkg bar 2>&1 | sanitize_pkg_digest bar.0.0.1 | tail -3
Error: Unable to resolve symlink
_build/_private/default/.pkg/bar.0.0.1-DIGEST_HASH/source/dir_b/link_to_a/link_to_b,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI isn't happy because it finds dir_a/link_to_b/link_to_a before the one I wrote in the test

@ElectreAAS ElectreAAS marked this pull request as ready for review April 9, 2026 16:20
@ElectreAAS
Copy link
Copy Markdown
Collaborator Author

Aside from the CI failures due to non-deterministic errors, the main code is ready for review.
Special attention should be given to the different Path.something.t, I had to hit them with a hammer until they looked nice, but I may very well have misunderstood the different invariants at play

@rgrinberg
Copy link
Copy Markdown
Member

I haven't looked closely, but I think your changes might have made that test non-deterministic.

Copy link
Copy Markdown
Collaborator

@Alizter Alizter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are some problems I've observed:

  1. There is a difference between local and tarball sources when it comes to directory symlinks. The tarball sources correctly resolve the contents wheras local sources silently discard them. I think rather than silently discarding them we should either raise a user error if this is something we wish not to support or support it. I would probably consider not supporting it.

  2. Broken symlinks appear to be silently ignored. For example something stupid like a symlink to itself. We should probably error to the user in this case saying that we don't accept such symlinks.

Comment thread src/dune_rules/pkg_rules.ml Outdated
Comment thread src/dune_pkg/fetch.ml Outdated
(* [raw_resolved] is a relative build path but it might contain indirections,
something like _build/foo/../bar
or _build/../outside *)
let canon_resolved = Path.of_string raw_resolved in
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only canonicalises relative paths and not absolute ones. I think Path.External.canonicalize_abs was the other way you did it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, raw_resolved is always a relative path, which is correctly canonicalized by Path.of_string
I ended up removing Path.External.canonicalize_abs anyway

Comment thread otherlibs/stdune/src/path.ml Outdated
Comment thread src/dune_rules/pkg_rules.ml Outdated
@Alizter
Copy link
Copy Markdown
Collaborator

Alizter commented Apr 14, 2026

Here are some of the issues we currently have:

Test symlink chains: A -> B -> C where intermediate links are also symlinks.

  $ mkdir -p _src/real_dir
  $ echo "content" > _src/real_dir/file.txt
  $ ln -s real_dir _src/link_a
  $ ln -s link_a _src/link_b
  $ ln -s link_b _src/link_c

  $ make_lockdir

Case 1: local source with symlink chain

For local sources, directory symlinks are not copied at all (they are skipped
during the source copy). Only the real directory is present.

  $ make_lockpkg foo <<EOF
  > (version 0.0.1)
  > (source
  >  (fetch
  >   (url file://$PWD/_src)))
  > (build (run cat real_dir/file.txt))
  > EOF

  $ build_pkg foo
  content

Only the real directory is copied - symlink chains are lost entirely:
  $ ls _build/_private/default/.pkg/foo.*/source | sort
  link_a
  link_b
  link_c
  real_dir

Case 2: tarball source with symlink chain

For tarballs, symlinks are preserved during extraction and then resolved.

  $ tar czf _src.tar.gz _src

  $ make_lockpkg bar <<EOF
  > (version 0.0.1)
  > (source
  >  (fetch
  >   (url file://$PWD/_src.tar.gz)))
  > (build (run cat real_dir/file.txt))
  > EOF

  $ build_pkg bar
  content

  $ ls _build/_private/default/.pkg/bar.*/source | sort
  link_a
  link_b
  link_c
  real_dir

BUG: Different behavior between local source and tarball. With local sources,
the symlink chain is completely lost (link_a, link_b, link_c are missing).
With tarballs, all links are resolved to real directories. This inconsistency
means a package behaves differently depending on how it is fetched.

@ElectreAAS ElectreAAS force-pushed the push-xmyqtonwkonu branch 2 times, most recently from bc64415 to 71bc486 Compare April 20, 2026 09:50
@ElectreAAS
Copy link
Copy Markdown
Collaborator Author

ElectreAAS commented Apr 20, 2026

Update: we've looked further into this and it seems the desired behaviour in Pkg_rules.source_files is rather complicated, almost equating actually supporting symlinks in the engine.
Since the point of this PR was to allow dune pkg to build fetched packages that may contain symlinks, and that we'd added the resolving pass in source_files only for symmetry, we came to the conclusion that it was unnecessary, and have removed it.

I've pushed a new version containing that deletion, along with a few WIP comments, I'm looking into them

@ElectreAAS ElectreAAS force-pushed the push-xmyqtonwkonu branch 5 times, most recently from bdb9bfd to 2ba65a9 Compare April 22, 2026 20:04
@ElectreAAS
Copy link
Copy Markdown
Collaborator Author

I've extracted a few unnecessary changes to #14291 to make review as simple as possible.
I think I've adressed all concerns raised previously, this is as ready as it will be

Alizter added a commit that referenced this pull request Apr 23, 2026
Just a little change extracted from #13792, nothing changing dune
behaviour.
Figured I'd use Fpath.traverse where it's appropriate
Alizter added a commit that referenced this pull request Apr 24, 2026
Just a trivial simplification in `Pkg_rules.source_files`, nothing
changing dune behaviour.

Instead of `map`ping the `relative` part later, do it once in the
partition.
This isn't particularly useful on its own, but in case of changes inside
the partition function, it makes debugging a lot easier
Extracted from #13792
Comment thread src/dune_pkg/fetch.ml Outdated
seen, if symlinks_in_children then Some Unix.S_DIR else None)
| { Unix.st_kind; _ } ->
(* We do not care about symlinks pointing to anything but directories. *)
seen, Some st_kind)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You say we don't care, but won't returning Some st_kind mean further processing by Fpath.traverse? Wouldn't it be better to actually skip it with None?

Comment thread src/dune_pkg/fetch.ml Outdated
seen, Some st_kind)
in
let _symlinks_seen : String.Set.t =
Fpath.traverse
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question about Fpath.traverse: Is it possible for this function to be "tricked" into reading more than it bargained for? i.e. can you create more things for it to read as it is reading?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My follow up question is what's the difference between the two cycle detection mechanisms:

  • seen
  • is_descendent

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question about Fpath.traverse: Is it possible for this function to be "tricked" into reading more than it bargained for? i.e. can you create more things for it to read as it is reading?

Yes it's possible. If on_symlink returns a Some kind, traverse's handle_kind will be called on that kind. You could even create an infinite loop by making on_symlink return a directory that contains a symlink, that will then be turned into yet another directory...

What I'm doing is obviously not that pathological case, but since I am turning directory symlinks into regular directories, I do create more stuff for the traverse to read. This is intended

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My follow up question is what's the difference between the two cycle detection mechanisms:
* seen
* is_descendent

I just checked and it turns out that seen is useless (at least in the tests we have so far). It was necessary at some point when is_descendant didn't work as I'd thought, but not anymore.
I'll try to stress-test it a little more, but maybe it can be removed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you can replace seen with () and simplify the rest a little bit more :-)

Comment thread src/dune_pkg/fetch.ml Outdated
Signed-off-by: Ambre Austen Suhamy <ambre@tarides.com>
… source)

Sort entries in fetch to guarantee determinism
Remove the symlink resolution happening in pkg_rules as it's too complicated.
The main change happening in fetch is still there.

Signed-off-by: Ambre Austen Suhamy <ambre@tarides.com>
The big comment explaining everything was moved to fetch.

Signed-off-by: Ambre Austen Suhamy <ambre@tarides.com>
Signed-off-by: Ambre Austen Suhamy <ambre@tarides.com>
Signed-off-by: Ambre Austen Suhamy <ambre@tarides.com>
Signed-off-by: Ambre Austen Suhamy <ambre@tarides.com>
…d function

Signed-off-by: Ambre Austen Suhamy <ambre@tarides.com>
Comment thread src/dune_pkg/fetch.ml Outdated
Remove 'seen' as cycle detection, is_descendant is enough

Signed-off-by: Ambre Austen Suhamy <ambre@tarides.com>
Signed-off-by: Ambre Austen Suhamy <ambre@tarides.com>
@ElectreAAS
Copy link
Copy Markdown
Collaborator Author

I tested this with ocamlbuild and fstar, and it builds fine!

@Alizter
Copy link
Copy Markdown
Collaborator

Alizter commented Apr 29, 2026

Those OxCaml failures are real and are due to a new version of ppx_expect. It seems let%test_module is now replaced by module%test. I will fix this in another PR.

Signed-off-by: Ali Caglayan <alizter@gmail.com>
@Alizter
Copy link
Copy Markdown
Collaborator

Alizter commented Apr 29, 2026

Annoyingly OxCaml is using ppx_expect v18, but v18 hasn't been released so we have a situation where the OxCaml version of the build is using a newer library that cannot overlap with the old version we have available. Luckily we can detect that and pass some compatibility flags to make it work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants